﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Timers;

namespace Frogs.Stability.LeakyBucket
{
	/// <summary>
	/// Default implementation of Frogs.Stability.LeakyBucket.ILeakyBucket
	/// </summary>
	public sealed class DefaultLeakyBucket : ILeakyBucket
	{
		IBucketTimer _dripTimer;

		int _retryLevel;
		int _maxLevel;
		int _drainRate;

		int DropCountValue { get; set; }

		BucketState WaterLevelValue { get; set; }

		/// <summary>
		/// Instantiates an instance of Frogs.Stability.LeakyBucket.DefaultLeakyBucket
		/// </summary>
		/// <param name="halfEmpty">Level at which to consider the bucket half empty</param>
		/// <param name="maxLevel">Maximum number of drops in bucket, must be greater than halfEmpty</param>
		/// <param name="drainRate">Number of drops drained per period</param>
		/// <param name="drainPeriod">Period of drain, specified in ms</param>
		public DefaultLeakyBucket(int halfEmpty, int maxLevel, int drainRate, int drainPeriod)
			: this(halfEmpty, maxLevel, drainRate, new DefaultBucketTimer(drainPeriod))
		{
		}

		/// <summary>
		/// Instantiates an instance of Frogs.Stability.LeakyBucket.DefaultLeakyBucket
		/// </summary>
		/// <param name="halfEmpty">Level at which to consider the bucket half empty</param>
		/// <param name="maxLevel">Maximum number of drops in bucket, must be greater than halfEmpty</param>
		/// <param name="drainRate">Number of drops drained per period</param>
		/// <param name="bucketTimer"></param>
		public DefaultLeakyBucket(int halfEmpty, int maxLevel, int drainRate, IBucketTimer bucketTimer)
		{
			if (halfEmpty >= maxLevel)
			{
				throw new ArgumentOutOfRangeException("halfEmpty must be less than maxLevel");
			}

			if (bucketTimer == null)
			{
				throw new ArgumentNullException("timer cannot be null");
			}

			this._retryLevel = halfEmpty;
			this._maxLevel = maxLevel;
			this._drainRate = drainRate;

			this.WaterLevel.Subscribe(level =>
			{
				this.WaterLevelValue = level;
			});

			this.DropCount.Subscribe(count =>
			{
				this.DropCountUpdated(count);
			});

			this._dripTimer = bucketTimer;
			this._dripTimer.Elapsed += (s, e) =>
			{
				RemoveDrops(this._drainRate);
			};
		}

		private void DropCountUpdated(int count)
		{
			this.DropCountValue = count;
			this.SetTimerState(count);

			switch (this.WaterLevelValue)
			{
				case BucketState.Empty:
					if (count == this._maxLevel)
					{
						this._stateStream.OnNext(BucketState.Full);
					}
					else if (count >= this._retryLevel)
					{
						this._stateStream.OnNext(BucketState.HalfFilled);
					}
					break;
				case BucketState.HalfDrained:
				case BucketState.HalfFilled:
					if (count == this._maxLevel)
					{
						this._stateStream.OnNext(BucketState.Full);
					}
					else if (count == 0)
					{
						this._stateStream.OnNext(BucketState.Empty);
					}
					break;
				case BucketState.Full:
					if (count == 0)
					{
						this._stateStream.OnNext(BucketState.Empty);
					}
					else if (count <= this._retryLevel)
					{
						this._stateStream.OnNext(BucketState.HalfDrained);
					}
					break;
				default:
					throw new NotImplementedException();
			}
		}

		private void SetTimerState(int count)
		{
			if (count > 0)
			{
				StartTimer();
			}
			else
			{
				StopTimer();
			}
		}

		private void StopTimer()
		{
			this._dripTimer.Stop();
		}

		private void StartTimer()
		{
			this._dripTimer.Start();
		}

		/// <summary>
		/// Add a single drop to the bucket
		/// </summary>
		public void AddSingleDrop()
		{
			this.AddDrops(1);
		}

		/// <summary>
		/// Add a specific number of drops to the bucket
		/// </summary>
		/// <param name="num"></param>
		public void AddDrops(int num)
		{
			var newCount = this.DropCountValue + num;
			if (newCount > this._maxLevel)
			{
				newCount = this._maxLevel;
			}

			this._countSubject.OnNext(newCount);
		}

		private void RemoveDrops(int num)
		{
			var newCount = this.DropCountValue - num;
			if (newCount < 0)
			{
				newCount = 0;
			}

			this._countSubject.OnNext(newCount);
		}

		/// <summary>
		/// Empty the bucket
		/// </summary>
		public void Empty()
		{
			this.StopTimer();

			if (this.DropCountValue != 0)
			{
				this._countSubject.OnNext(0);
			}

			if (this.WaterLevelValue != BucketState.Empty)
			{
				this._stateStream.OnNext(LeakyBucket.BucketState.Empty);
			}
		}

		/// <summary>
		/// Instantly fill the bucket
		/// </summary>
		public void FillUp()
		{
			this.AddDrops(this._maxLevel);
		}

		Subject<BucketState> _stateStream = new Subject<BucketState>();
		/// <summary>
		/// Exposes the bucket state
		/// </summary>
		public IObservable<BucketState> WaterLevel { get { return _stateStream; } }

		Subject<int> _countSubject = new Subject<int>();
		IObservable<int> DropCount
		{
			get
			{
				return _countSubject;
			}
		}

	}
}
